package com.googlecode.biglog.appender;

import java.lang.reflect.Field;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import net.sf.common.util.ReflectionUtils;
import net.sf.common.util.ValueUtils;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.LocationInfo;
import org.apache.log4j.spi.LoggingEvent;
import org.codehaus.jackson.map.ObjectMapper;

import com.googlecode.biglog.annotation.LogClass;
import com.googlecode.biglog.annotation.LogLevel;
import com.googlecode.biglog.annotation.LogLine;
import com.googlecode.biglog.annotation.LogMessage;
import com.googlecode.biglog.annotation.LogMethod;
import com.googlecode.biglog.annotation.LogThread;
import com.googlecode.biglog.annotation.LogTime;

public abstract class AbstractModelAppender extends AppenderSkeleton {
	private String prefix;
	private String model;
	private String modelName;
	private String timeFieldName = "time";
	private String levelFieldName = "level";
	private String threadFieldName = "thread";
	private String classFieldName = "clazz";
	private String methodFieldName = "method";
	private String lineFieldName = "line";
	private String messageFieldName = "message";
	public String getModel() {
		return model;
	}
	public void setModel(String logModel) {
		this.model = logModel;
	}
	public String getModelName() {
		return modelName;
	}
	public void setModelName(String modelName) {
		this.modelName = modelName;
	}
	public String getPrefix() {
		return prefix;
	}
	public void setPrefix(String prefix) {
		this.prefix = prefix;
	}
	public String getTimeFieldName() {
		return timeFieldName;
	}
	public void setTimeFieldName(String timeFieldName) {
		this.timeFieldName = timeFieldName;
	}
	public String getLevelFieldName() {
		return levelFieldName;
	}
	public void setLevelFieldName(String levelFieldName) {
		this.levelFieldName = levelFieldName;
	}
	public String getThreadFieldName() {
		return threadFieldName;
	}
	public void setThreadFieldName(String threadFieldName) {
		this.threadFieldName = threadFieldName;
	}
	public String getClassFieldName() {
		return classFieldName;
	}
	public void setClassFieldName(String classFieldName) {
		this.classFieldName = classFieldName;
	}
	public String getMethodFieldName() {
		return methodFieldName;
	}
	public void setMethodFieldName(String methodFieldName) {
		this.methodFieldName = methodFieldName;
	}
	public String getLineFieldName() {
		return lineFieldName;
	}
	public void setLineFieldName(String lineFieldName) {
		this.lineFieldName = lineFieldName;
	}
	public String getMessageFieldName() {
		return messageFieldName;
	}
	public void setMessageFieldName(String messageFieldName) {
		this.messageFieldName = messageFieldName;
	}

	private Class<?> modelClass;
	private boolean initialized = false;
	public Class<?> getModelClass() {
		return modelClass;
	}
	public void setModelClass(Class<?> modelClass) {
		this.modelClass = modelClass;
	}
	public boolean isInitialized() {
		return initialized;
	}

	@Override
	public boolean requiresLayout() {
		return false;
	}

	@Override
	public void activateOptions() {
		super.activateOptions();

		try {
			close();
			if (ValueUtils.isEmpty(model)) {
				if (ValueUtils.isEmpty(modelName))
					throw new IllegalArgumentException("model or modelName property is required.");
			} else {
				modelClass = Class.forName(model);

				for (Field field : ReflectionUtils.getFieldList(modelClass, false)) {
					LogTime time = field.getAnnotation(LogTime.class);
					if (time != null) {
						timeFieldName = field.getName();
						continue;
					}
					LogLevel level = field.getAnnotation(LogLevel.class);
					if (level != null) {
						levelFieldName = field.getName();
						continue;
					}
					LogThread thread = field.getAnnotation(LogThread.class);
					if (thread != null) {
						threadFieldName = field.getName();
						continue;
					}
					LogClass clazz = field.getAnnotation(LogClass.class);
					if (clazz != null) {
						classFieldName = field.getName();
						continue;
					}
					LogMethod method = field.getAnnotation(LogMethod.class);
					if (method != null) {
						methodFieldName = field.getName();
						continue;
					}
					LogLine line = field.getAnnotation(LogLine.class);
					if (line != null) {
						lineFieldName = field.getName();
						continue;
					}
					LogMessage message = field.getAnnotation(LogMessage.class);
					if (message != null) {
						messageFieldName = field.getName();
						continue;
					}
				}
			}

			initialized = true;
		} catch (Exception e) {
			errorHandler.error("Unexpected exception while initialising MongoDbAppender.", e, ErrorCode.GENERIC_FAILURE);
		}
	}

	@SuppressWarnings("unchecked")
	protected Map<String, Object> toMap(LoggingEvent loggingEvent) {
		if (loggingEvent == null || !isInitialized())
			return null;

		String message = loggingEvent.getRenderedMessage();
		String msg = message;
		if (msg != null) {
			msg = msg.trim();
			String prefix = getPrefix();
			if (prefix != null && !msg.startsWith(prefix)) {
				msg = null;
			} else {
				msg = msg.substring(prefix.length());
				msg = msg.trim();
			}
		}

		if (msg == null || !msg.startsWith("{") || !msg.endsWith("}")) {
			Map<String, Object> map = new HashMap<String, Object>();
			map.put(getTimeFieldName(), new Date(loggingEvent.getTimeStamp()));
			map.put(getLevelFieldName(), loggingEvent.getLevel().toString());
			map.put(getThreadFieldName(), loggingEvent.getThreadName());
			LocationInfo locInfo = loggingEvent.getLocationInformation();
			map.put(getClassFieldName(), locInfo.getClassName());
			map.put(getMethodFieldName(), locInfo.getMethodName());
			map.put(getLineFieldName(), locInfo.getLineNumber());
			map.put(getMessageFieldName(), message);
			return map;
		}

		Map<String, Object> map = null;
		ObjectMapper mapper = new ObjectMapper();
		try {
			map = (Map<String, Object>) mapper.readValue(msg, Map.class);
			if (!map.containsKey(getTimeFieldName()))
				map.put(getTimeFieldName(), new Date(loggingEvent.getTimeStamp()));
			if (!map.containsKey(getLevelFieldName()))
				map.put(getLevelFieldName(), loggingEvent.getLevel().toString());
			if (!map.containsKey(getThreadFieldName()))
				map.put(getThreadFieldName(), loggingEvent.getThreadName());
			LocationInfo locInfo = loggingEvent.getLocationInformation();
			if (!map.containsKey(getClassFieldName()))
				map.put(getClassFieldName(), locInfo.getClassName());
			if (!map.containsKey(getMethodFieldName()))
				map.put(getMethodFieldName(), locInfo.getMethodName());
			if (!map.containsKey(getLineFieldName()))
				map.put(getLineFieldName(), locInfo.getLineNumber());
			if (!map.containsKey(getMessageFieldName()))
				map.put(getMessageFieldName(), message);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}

		return map;
	}
}
